import asyncio
import os
import sys
import json
import threading
import logging
import re
from typing import List, Dict, Optional
from datetime import datetime
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtCore import Qt, QPropertyAnimation, QEasingCurve
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QLineEdit

from api_client import (
    get_customer_data,
    get_talking_status,
    get_chat_data,
    send_sms,
    get_redis_value,
    set_redis_value,
    publish_redis, set_guaduan_phone,
)
from jindie.jd_task import main_task
from simple_multi_device_doubao import SimpleMultiDeviceManager
from config_manager import get_config_manager


def get_resource_path(filename: str) -> str:
    """获取资源文件路径，兼容打包环境"""
    if hasattr(sys, "_MEIPASS"):
        candidate = os.path.join(sys._MEIPASS, filename)
    else:
        candidate = filename
    return candidate if os.path.exists(candidate) else filename


# 新增色彩变量定义
COLORS = {
    "primary_orange": "#3b82f6",
    "primary_blue": "#2563eb",
    "text_primary": "#e0e0e0",
    "text_secondary": "#a0a0a0",
    "background": "#121212",
    "card_bg": "#1e1e1e",
    "border": "#2a2a2a",
    "success": "#4ade80",
    "warning": "#fbbf24",
    "error": "#f87171",
    "dark_text_primary": "#e0e0e0",
    "dark_background": "#121212",
    "dark_card_bg": "#1e1e1e",
    "dark_border": "#2a2a2a",
}

AUTO_NEXT_DIAL_KEY_PREFIX = "auto_next_dial:"
def get_global_styles() -> str:
    """统一管理全局样式"""
    return f"""
        QMainWindow {{
            background-color: {COLORS['background']};
            color: {COLORS['text_primary']};
        }}
        QWidget {{
            font-family: "Microsoft YaHei UI", "思源黑体";
            font-size: 13px;
            color: {COLORS['text_primary']};
        }}
        QLabel[secondary="true"] {{
            color: {COLORS['text_secondary']};
        }}
        QTabWidget {{
            background-color: {COLORS['background']};
            border: none;
        }}
        QTabWidget::pane {{
            background-color: {COLORS['card_bg']};
            border: 1px solid {COLORS['border']};
            border-radius: 14px;
            padding: 12px;
        }}
        QTabBar::tab {{
            background-color: transparent;
            color: {COLORS['text_secondary']};
            padding: 10px 24px;
            border-radius: 12px;
            margin-right: 8px;
            border: 1px solid transparent;
        }}
        QTabBar::tab:selected {{
            color: {COLORS['text_primary']};
            border-color: {COLORS['primary_orange']};
            background-color: rgba(59, 130, 246, 0.08);
        }}
        QTabBar::tab:hover:!selected {{
            color: #ffffff;
            border-color: {COLORS['border']};
        }}
        QLineEdit, QTextEdit, QPlainTextEdit, QComboBox {{
            border: 1px solid {COLORS['border']};
            border-radius: 6px;
            padding: 6px 10px;
            min-height: 28px;
            background-color: #181818;
            color: {COLORS['text_primary']};
        }}
        QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus, QComboBox:focus {{
            border-color: {COLORS['primary_orange']};
        }}
        QScrollArea {{
            border: none;
            background-color: transparent;
        }}
        QScrollBar:vertical, QScrollBar:horizontal {{
            background: #1b1b1b;
            border-radius: 6px;
            width: 10px;
            height: 10px;
            margin: 4px;
        }}
        QScrollBar::handle:vertical, QScrollBar::handle:horizontal {{
            background: #2c2c2c;
            border-radius: 6px;
        }}
        QScrollBar::handle:vertical:hover, QScrollBar::handle:horizontal:hover {{
            background: #3a3a3a;
        }}
        QWidget#device_card {{
            background-color: {COLORS['card_bg']};
            border: 1px solid {COLORS['border']};
            border-radius: 12px;
            padding: 16px;
            transition: all 0.2s ease;
        }}
        QWidget#device_card:hover {{
            border-color: {COLORS['primary_orange']};
            margin-top: -2px;
            box-shadow: 0 12px 24px rgba(0,0,0,0.25);
        }}
        QWidget#device_card[selected="true"] {{
            border-color: {COLORS['primary_orange']};
            box-shadow: 0 0 0 1px {COLORS['primary_orange']};
        }}
        QWidget#status_card {{
            background-color: {COLORS['card_bg']};
            border: 1px solid {COLORS['border']};
            border-radius: 16px;
            padding: 20px;
        }}
        QLabel#status_online {{
            color: {COLORS['success']};
            font-weight: 600;
        }}
        QLabel#status_offline {{
            color: {COLORS['error']};
            font-weight: 600;
        }}
    """


BUTTON_STYLE_PRESETS = {
    "primary": f"""
        QPushButton {{
            background-color: {COLORS['primary_orange']};
            color: #ffffff;
            border: none;
            border-radius: 6px;
            padding: 6px 14px;
            font-weight: 600;
            min-height: 30px;
            font-size: 12px;
        }}
        QPushButton:hover {{
            background-color: {COLORS['primary_blue']};
        }}
        QPushButton:pressed {{
            padding-top: 7px;
            padding-bottom: 5px;
        }}
        QPushButton:disabled {{
            background-color: #2a2a2a;
            color: #666666;
        }}
    """,
    "secondary": f"""
        QPushButton {{
            background-color: #1f1f1f;
            color: {COLORS['text_primary']};
            border: 1px solid {COLORS['border']};
            border-radius: 6px;
            padding: 6px 14px;
            font-weight: 600;
            min-height: 28px;
            font-size: 12px;
        }}
        QPushButton:hover {{
            border-color: {COLORS['primary_orange']};
            color: #ffffff;
        }}
        QPushButton:pressed {{
            padding-top: 7px;
            padding-bottom: 5px;
        }}
    """,
    "tertiary": f"""
        QPushButton {{
            background-color: transparent;
            color: {COLORS['text_secondary']};
            border: 1px dashed {COLORS['border']};
            border-radius: 6px;
            padding: 6px 14px;
            font-weight: 500;
            min-height: 28px;
            font-size: 12px;
        }}
        QPushButton:hover {{
            color: {COLORS['primary_orange']};
            border-color: {COLORS['primary_orange']};
        }}
        QPushButton:pressed {{
            padding-top: 7px;
            padding-bottom: 5px;
        }}
    """,
    "danger": f"""
        QPushButton {{
            background-color: {COLORS['error']};
            color: #ffffff;
            border: none;
            border-radius: 6px;
            padding: 6px 14px;
            font-weight: 600;
            min-height: 30px;
            font-size: 12px;
        }}
        QPushButton:hover {{
            background-color: #dc2626;
        }}
        QPushButton:pressed {{
            padding-top: 7px;
            padding-bottom: 5px;
        }}
    """,
    "icon": f"""
        QPushButton {{
            background-color: transparent;
            border: none;
            color: {COLORS['text_secondary']};
            border-radius: 8px;
            width: 36px;
            height: 36px;
        }}
        QPushButton:hover {{
            background-color: rgba(255,255,255,0.08);
            color: #ffffff;
        }}
        QPushButton:pressed {{
            background-color: rgba(255,255,255,0.15);
        }}
    """,
}


def load_svg_icon(svg_data: str, size: int = 16) -> QtGui.QIcon:
    """从SVG字符串创建图标"""
    if not svg_data:
        return QtGui.QIcon()
    image = QtGui.QImage.fromData(svg_data.encode("utf-8"))
    pixmap = QtGui.QPixmap.fromImage(image).scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
    icon = QtGui.QIcon()
    icon.addPixmap(pixmap)
    return icon


ICON_LIBRARY = {
    "load": """
        <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='#e0e0e0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>
            <path d='M12 3v12'/>
            <path d='m8 11 4 4 4-4'/>
            <path d='M5 21h14'/>
        </svg>
    """,
    "save": """
        <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='#e0e0e0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>
            <path d='M20 21H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z'/>
            <path d='M17 21v-8H7v8'/>
            <path d='M7 3v5h8'/>
        </svg>
    """,
    "import": """
        <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='#e0e0e0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>
            <path d='M12 3v12'/>
            <path d='m8 11 4 4 4-4'/>
            <path d='M20 21H4'/>
        </svg>
    """,
    "export": """
        <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='#e0e0e0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>
            <path d='M12 21V9'/>
            <path d='m16 13-4-4-4 4'/>
            <path d='M4 3h16'/>
        </svg>
    """,
    "reset": """
        <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='#e0e0e0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>
            <path d='M3 12a9 9 0 1 0 9-9'/>
            <path d='M3 4v4h4'/>
        </svg>
    """,
}
def get_button_style(variant: str = "primary") -> str:
    """获取按钮样式"""
    return BUTTON_STYLE_PRESETS.get(variant, BUTTON_STYLE_PRESETS["primary"])


def apply_button_style(button: QtWidgets.QPushButton, variant: str = "primary"):
    """应用按钮样式"""
    button.setStyleSheet(get_button_style(variant))
    button.setCursor(QtGui.QCursor(Qt.PointingHandCursor))


def get_log_text_style() -> str:
    """日志文本框样式"""
    return f"""
        QPlainTextEdit {{
            background-color: {COLORS['card_bg']};
            color: {COLORS['text_primary']};
            font-size: 13px;
            line-height: 1.8;
            border: 1px solid {COLORS['border']};
            border-radius: 12px;
            padding: 14px;
        }}
        QPlainTextEdit:focus {{
            border-color: {COLORS['primary_orange']};
        }}
    """


def create_layout(layout_cls, parent=None, margins=(0, 0, 0, 0), spacing=0):
    """通用布局创建函数"""
    layout = layout_cls(parent) if parent else layout_cls()
    layout.setContentsMargins(*margins)
    layout.setSpacing(spacing)
    return layout


def create_v_layout(parent=None, margins=(0, 0, 0, 0), spacing=0):
    """创建垂直布局"""
    return create_layout(QtWidgets.QVBoxLayout, parent, margins, spacing)


def create_h_layout(parent=None, margins=(0, 0, 0, 0), spacing=0):
    """创建水平布局"""
    return create_layout(QtWidgets.QHBoxLayout, parent, margins, spacing)


def add_widgets_to_layout(layout, widgets):
    """批量添加控件到布局"""
    for widget in widgets:
        if isinstance(widget, str) and widget == "stretch":
            layout.addStretch()
        elif isinstance(widget, QtWidgets.QLayout):
            layout.addLayout(widget)
        elif widget is not None:
            layout.addWidget(widget)


def create_config_button(text: str, icon_key: str, variant: str, tooltip: str) -> QtWidgets.QPushButton:
    btn = QtWidgets.QPushButton(text)
    apply_button_style(btn, variant)
    btn.setMinimumWidth(100)
    btn.setIcon(load_svg_icon(ICON_LIBRARY.get(icon_key, "")))
    btn.setIconSize(QtCore.QSize(16, 16))
    btn.setToolTip(tooltip)
    btn.setCursor(QtGui.QCursor(Qt.PointingHandCursor))
    return btn

class ToggleSwitch(QtWidgets.QCheckBox):
    """Simple pill-style toggle switch."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setCursor(QtGui.QCursor(Qt.PointingHandCursor))
        self.setTristate(False)
        self.setText("")
        self.setFocusPolicy(Qt.NoFocus)
        self._size = QtCore.QSize(48, 24)
        self.setFixedSize(self._size)

    def sizeHint(self):
        return self._size

    def paintEvent(self, event):
        """Custom drawing to mimic iOS-style switch."""
        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)

        rect = self.rect()
        radius = rect.height() / 2
        bg_color = QtGui.QColor("#3b82f6" if self.isChecked() else "#4b5563")

        painter.setPen(QtCore.Qt.NoPen)
        painter.setBrush(bg_color)
        painter.drawRoundedRect(rect.adjusted(0, 0, -1, -1), radius, radius)

        knob_diameter = rect.height() - 6
        knob_y = (rect.height() - knob_diameter) / 2
        knob_x = rect.width() - knob_diameter - 3 if self.isChecked() else 3

        painter.setBrush(QtGui.QColor("#ffffff"))
        painter.drawEllipse(QtCore.QRectF(knob_x, knob_y, knob_diameter, knob_diameter))

# 线程本地存储，用于存储当前设备的标识
_thread_local = threading.local()
# 线程ID到设备手机号的映射（作为备选方案）
_thread_device_map: Dict[int, str] = {}
_thread_device_map_lock = threading.Lock()


def _get_device_context() -> Optional[str]:
    """获取当前线程的设备上下文"""
    device_phone = getattr(_thread_local, 'device_phone', None)
    if device_phone:
        return device_phone
    thread_id = threading.get_ident()
    with _thread_device_map_lock:
        return _thread_device_map.get(thread_id)


def _set_device_context(device_phone: Optional[str]):
    """设置当前线程的设备上下文"""
    _thread_local.device_phone = device_phone
    thread_id = threading.get_ident()
    with _thread_device_map_lock:
        if device_phone:
            _thread_device_map[thread_id] = device_phone
        else:
            _thread_device_map.pop(thread_id, None)


class DeviceCard(QtWidgets.QWidget):
    """设备卡片展示与输入组件"""

    selection_changed = QtCore.pyqtSignal()
    detail_requested = QtCore.pyqtSignal(object)
    stop_ai_requested = QtCore.pyqtSignal(str)  # device_phone

    def __init__(self, device_info: Optional[Dict] = None, parent=None):
        super().__init__(parent)
        device_info = device_info or {}
        self.setObjectName("device_card")
        self.setProperty("selected", False)

        self._selected = False
        self.is_online = False
        self.connected_at: Optional[datetime] = None
        self._auto_dial_phone: Optional[str] = None
        self._auto_dial_state: bool = True

        self.mic_edit = QtWidgets.QLineEdit(device_info.get("input_name") or device_info.get("mic", ""))
        self.mic_edit.setPlaceholderText("麦克风名称")
        self.speaker_edit = QtWidgets.QLineEdit(device_info.get("output_name") or device_info.get("speaker", ""))
        self.speaker_edit.setPlaceholderText("扬声器名称")
        self.phone_edit = QtWidgets.QLineEdit(device_info.get("phone", ""))
        self.phone_edit.setPlaceholderText("手机号 (11位)")

        self.status_indicator = QtWidgets.QLabel()
        self.status_indicator.setFixedSize(12, 12)
        self.status_label = QtWidgets.QLabel("离线")

        self.phone_display = QtWidgets.QLabel(self._format_phone(self.phone_edit.text()))
        self.phone_display.setStyleSheet("font-size: 16px; font-weight: 600;")

        main_layout = create_v_layout(self, margins=(16, 16, 16, 16), spacing=12)
        header = create_h_layout(spacing=10)
        status_row = create_h_layout(spacing=6)
        status_row.addWidget(self.status_indicator)
        status_row.addWidget(self.status_label)
        header.addLayout(status_row)
        header.addWidget(self.phone_display)
        header.addStretch()

        self.select_box = QtWidgets.QCheckBox("选中")
        self.select_box.stateChanged.connect(lambda s: self.set_selected(s == Qt.Checked))
        header.addWidget(self.select_box)

        self.detail_btn = QtWidgets.QPushButton("详情")
        apply_button_style(self.detail_btn, "secondary")
        self.detail_btn.clicked.connect(self._emit_detail)
        header.addWidget(self.detail_btn)
        
        # 添加停止AI按钮
        self.stop_ai_btn = QtWidgets.QPushButton("停止AI")
        apply_button_style(self.stop_ai_btn, "danger")
        self.stop_ai_btn.setToolTip("断开该设备的AI回复WebSocket连接")
        header.addWidget(self.stop_ai_btn)



        # 自动拨号开关和下一通操作
        self.auto_dial_label = QtWidgets.QLabel("自动拨号")
        self.auto_dial_label.setProperty("secondary", True)
        header.addWidget(self.auto_dial_label)

        self.auto_dial_switch = ToggleSwitch()
        self.auto_dial_switch.setEnabled(False)
        self.auto_dial_switch.toggled.connect(self._handle_auto_dial_toggle)
        header.addWidget(self.auto_dial_switch)

        self.next_call_btn = QtWidgets.QPushButton("下一通")
        apply_button_style(self.next_call_btn, "primary")
        self.next_call_btn.setVisible(False)
        self.next_call_btn.clicked.connect(self._publish_next_call)
        header.addWidget(self.next_call_btn)

        self.end_call_btn = QtWidgets.QPushButton("结束通话")
        apply_button_style(self.end_call_btn, "danger")
        self.end_call_btn.clicked.connect(self._publish_end_call)
        header.addWidget(self.end_call_btn)

        # 发送按钮
        self.send_sms_btn = QtWidgets.QPushButton("发送短信")
        apply_button_style(self.send_sms_btn, "primary")
        self.send_sms_btn.clicked.connect(self._send_sms)
        # dial_header.addWidget(self.send_sms_btn)
        header.addWidget(self.send_sms_btn)

        main_layout.addLayout(header)

        form = QtWidgets.QGridLayout()
        form.setHorizontalSpacing(12)
        form.setVerticalSpacing(10)
        form.addWidget(QtWidgets.QLabel("麦克风"), 0, 0)
        form.addWidget(self.mic_edit, 0, 1)
        form.addWidget(QtWidgets.QLabel("扬声器"), 0, 2)
        form.addWidget(self.speaker_edit, 0, 3)
        form.addWidget(QtWidgets.QLabel("手机号"), 1, 0)
        form.addWidget(self.phone_edit, 1, 1, 1, 3)
        main_layout.addLayout(form)

        # 拨号状态显示区域
        dial_status_layout = create_v_layout(spacing=8)
        # 设置当前拨号客户信息
        self.dial_customer_label = QtWidgets.QLabel("拨号客户：无")
        self.dial_customer_label.setProperty("secondary", "true")
        dial_status_layout.addWidget(self.dial_customer_label)

        # 当前拨号手机号
        self.dial_phone_label = QtWidgets.QLabel("当前拨号：无")
        self.dial_phone_label.setProperty("secondary", "true")
        dial_status_layout.addWidget(self.dial_phone_label)

        # 拨号状态
        self.dial_status_label = QtWidgets.QLabel("状态：未拨号")
        self.dial_status_label.setProperty("secondary", "true")
        dial_status_layout.addWidget(self.dial_status_label)

        main_layout.addLayout(dial_status_layout)
        self.setLayout(main_layout)

        shadow = QtWidgets.QGraphicsDropShadowEffect()
        shadow.setColor(QtGui.QColor(0, 0, 0, 90))
        shadow.setBlurRadius(24)
        shadow.setOffset(0, 10)
        self.setGraphicsEffect(shadow)

        self.phone_edit.textChanged.connect(self._handle_phone_changed)
        
        # 连接按钮信号
        self.stop_ai_btn.clicked.connect(self._emit_stop_ai)
        self._load_dial_status()
        self.set_status(False)
        self._init_auto_dial_state(initial=True)

        # 定时刷新拨号状态（每3秒刷新一次）
        self.refresh_timer = QtCore.QTimer()
        self.refresh_timer.timeout.connect(self._refresh_data)
        self.refresh_timer.start(3000)  # 3秒刷新一次

    def _refresh_data(self):
        """定时刷新数据"""
        # 刷新拨号状态
        self._load_dial_status()


    def _load_dial_status(self):
        """加载拨号状态（业务逻辑待实现）"""
        # TODO: 实现获取当前拨号状态的业务逻辑
        customer_data = get_customer_data(self.phone_edit.text().strip())
        dial_phone = customer_data.get("phone","")
        dial_status = get_talking_status(self.phone_edit.text().strip())
        if dial_phone:
            self.dial_customer_label.setText(f"客户 姓名：{customer_data.get('customer_name')} 年龄：{customer_data.get('age')}")
            self.dial_phone_label.setText(f"当前拨号：{dial_phone}")
            self.dial_status_label.setText(f"状态：{dial_status}")
        else:
            self.dial_phone_label.setText("当前拨号：无")
            self.dial_status_label.setText("状态：未拨号")

    def _send_sms(self):
        """发送短信（业务逻辑待实现）"""
        # TODO: 实现发送短信的业务逻辑
        # 示例：
        phone = self.phone_edit.text().strip()
        customer_data = get_customer_data(phone)
        called_phone = customer_data.get("phone", "")
        if not called_phone:
            CustomMessageBox("失败", "短信发送失败", "error", self).exec_()
            return
        result = send_sms(called_phone = called_phone, device_phone = phone)
        if result:
            CustomMessageBox("成功", "短信发送成功", "info", self).exec_()
            # self._load_chat_history()  # 刷新聊天记录
        else:
            CustomMessageBox("失败", "短信发送失败", "error", self).exec_()
    def _format_phone(self, phone: str) -> str:
        phone = phone.strip()
        if len(phone) == 11:
            return f"{phone[:3]}****{phone[-4:]}"
        return phone or "未设置手机号"

    def _sync_phone_display(self, text: str):
        self.phone_display.setText(self._format_phone(text))

    def _handle_phone_changed(self, text: str):
        """同步显示并根据手机号刷新自动拨号状态"""
        self._sync_phone_display(text)
        self._init_auto_dial_state()

    def _init_auto_dial_state(self, initial: bool = False):
        """在Redis中确保自动拨号键存在并刷新UI"""
        phone = self.phone_edit.text().strip()
        if len(phone) != 11:
            self._auto_dial_phone = None
            self._set_auto_dial_controls_enabled(False)
            return

        if not initial and phone == self._auto_dial_phone:
            return

        self._auto_dial_phone = phone
        key = self._build_auto_dial_key()

        try:
            value = get_redis_value(key)
        except Exception as exc:
            logging.error("读取自动拨号状态失败: %s", exc)
            self._set_auto_dial_controls_enabled(False)
            return

        if value is None:
            try:
                set_redis_value(key, "yes")
                value = "yes"
            except Exception as exc:
                logging.error("初始化自动拨号状态失败: %s", exc)
                self._set_auto_dial_controls_enabled(False)
                return

        self._set_auto_dial_controls_enabled(True)
        self._apply_auto_dial_state(value.lower() != "no", sync=False)

    def _build_auto_dial_key(self) -> Optional[str]:
        phone = self.phone_edit.text().strip()
        if len(phone) != 11:
            return None
        return f"{AUTO_NEXT_DIAL_KEY_PREFIX}{phone}"

    def _set_auto_dial_controls_enabled(self, enabled: bool):
        self.auto_dial_switch.setEnabled(enabled)
        self.auto_dial_label.setEnabled(enabled)
        if not enabled:
            self.next_call_btn.setVisible(False)
            self.next_call_btn.setEnabled(False)
        else:
            self._refresh_auto_dial_ui()

    def _apply_auto_dial_state(self, enabled: bool, sync: bool = True):
        self._auto_dial_state = enabled
        self.auto_dial_switch.blockSignals(True)
        self.auto_dial_switch.setChecked(enabled)
        self.auto_dial_switch.blockSignals(False)
        self._refresh_auto_dial_ui()
        if sync:
            self._write_auto_dial_state()

    def _refresh_auto_dial_ui(self):
        is_enabled = self.auto_dial_switch.isEnabled()
        tooltip = "自动拨号已开启" if self._auto_dial_state else "自动拨号已关闭"
        self.auto_dial_switch.setToolTip(tooltip)
        self.next_call_btn.setVisible(is_enabled and not self._auto_dial_state)
        self.next_call_btn.setEnabled(is_enabled and not self._auto_dial_state)

    def _handle_auto_dial_toggle(self, checked: bool):
        if not self.auto_dial_switch.isEnabled():
            return
        self._apply_auto_dial_state(checked, sync=True)

    def _write_auto_dial_state(self):
        key = self._build_auto_dial_key()
        if not key:
            return
        value = "yes" if self._auto_dial_state else "no"
        try:
            set_redis_value(key, value)
        except Exception as exc:
            logging.error("更新自动拨号状态失败: %s", exc)

    def _publish_next_call(self):
        key = self._build_auto_dial_key()
        if not key:
            return
        try:
            publish_redis(key, "yes")
            QtWidgets.QToolTip.showText(
                QtGui.QCursor.pos(),
                "已触发下一通",
                self.next_call_btn,
            )
        except Exception as exc:
            logging.error("发布下一通消息失败: %s", exc)

    def _publish_end_call(self):
        device_phone = self.phone_edit.text().strip()
        status = get_talking_status(device_phone=device_phone)
        if status =="dialing" or status =="connected":
            set_guaduan_phone(device_phone=device_phone,status="guaduan")
            CustomMessageBox("成功", "挂断成功", "info", self).exec_()
        else:
            CustomMessageBox("失败", "当前手机没有进行任何通话", "error", self).exec_()

    def set_selected(self, selected: bool):
        self._selected = selected
        self.select_box.blockSignals(True)
        self.select_box.setChecked(selected)
        self.select_box.blockSignals(False)
        self.setProperty("selected", selected)
        self.style().unpolish(self)
        self.style().polish(self)
        self.selection_changed.emit()

    def is_selected(self) -> bool:
        return self._selected

    def set_status(self, is_online: bool):
        self.is_online = is_online
        color = COLORS['success'] if is_online else COLORS['error']
        self.status_indicator.setStyleSheet(f"background-color: {color}; border-radius: 6px;")
        self.status_label.setText("在线" if is_online else "离线")
        if is_online:
            self.connected_at = self.connected_at or datetime.now()
        else:
            self.connected_at = None

    def get_data(self) -> Dict[str, str]:
        return {
            "input_name": self.mic_edit.text().strip(),
            "output_name": self.speaker_edit.text().strip(),
            "phone": self.phone_edit.text().strip(),
        }

    def update_fields(self, mic: str, speaker: str, phone: str):
        self.mic_edit.setText(mic)
        self.speaker_edit.setText(speaker)
        self.phone_edit.setText(phone)
        self._sync_phone_display(phone)

    def set_editable(self, editable: bool):
        for widget in (self.mic_edit, self.speaker_edit, self.phone_edit):
            widget.setReadOnly(not editable)

    def matches_phone(self, phone: str) -> bool:
        return self.phone_edit.text().strip() == phone.strip()

    def mouseDoubleClickEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._emit_detail()
        super().mouseDoubleClickEvent(event)

    def _emit_detail(self):
        self.detail_requested.emit(self)
    
    def _emit_stop_ai(self):
        """发送停止AI信号"""
        phone = self.phone_edit.text().strip()
        if phone:
            self.stop_ai_requested.emit(phone)
    


class DeviceDetailDialog(QtWidgets.QDialog):
    """设备详情弹窗"""

    def __init__(self, device_card: DeviceCard, parent=None):
        super().__init__(parent)
        self.device_card = device_card
        self.setWindowTitle("设备详情")
        self.setModal(True)
        self.setFixedSize(600, 700)  # 增大尺寸以容纳新功能
        self.setStyleSheet(f"""
            QDialog {{
                background-color: {COLORS['card_bg']};
                border-radius: 12px;
                border: 1px solid {COLORS['border']};
            }}
            QLabel {{
                color: {COLORS['text_primary']};
            }}
            QLabel[secondary="true"] {{
                color: {COLORS['text_secondary']};
            }}
        """)

        main_layout = create_v_layout(self, margins=(20, 20, 20, 20), spacing=16)

        # 使用标签页组织内容
        self.tabs = QtWidgets.QTabWidget()
        self.tabs.setStyleSheet(f"""
            QTabWidget::pane {{
                background-color: {COLORS['card_bg']};
                border: 1px solid {COLORS['border']};
                border-radius: 8px;
            }}
            QTabBar::tab {{
                background-color: transparent;
                color: {COLORS['text_secondary']};
                padding: 8px 16px;
                border-radius: 6px;
                margin-right: 4px;
            }}
            QTabBar::tab:selected {{
                color: {COLORS['text_primary']};
                border-bottom: 2px solid {COLORS['primary_orange']};
            }}
        """)

        # 基本信息标签页
        self.info_tab = self._create_info_tab()
        self.tabs.addTab(self.info_tab, "基本信息")

        # 聊天记录标签页
        self.chat_tab = self._create_chat_tab()
        self.tabs.addTab(self.chat_tab, "聊天记录")

        main_layout.addWidget(self.tabs)

        # 底部按钮
        btn_row = create_h_layout(spacing=10)
        self.test_btn = QtWidgets.QPushButton("测试设备")
        self.save_btn = QtWidgets.QPushButton("修改配置")
        apply_button_style(self.test_btn, "secondary")
        apply_button_style(self.save_btn, "primary")
        btn_row.addWidget(self.test_btn)
        btn_row.addWidget(self.save_btn)
        main_layout.addLayout(btn_row)

        self.test_btn.clicked.connect(self._test_device)
        self.save_btn.clicked.connect(self._apply_changes)

        # 初始化数据
        self._load_dial_status()
        self._load_chat_history()

        # 定时刷新拨号状态和聊天记录（每3秒刷新一次）
        # self.refresh_timer = QtCore.QTimer()
        # self.refresh_timer.timeout.connect(self._refresh_data)
        # self.refresh_timer.start(3000)  # 3秒刷新一次

    # def _refresh_data(self):
    #     """定时刷新数据"""
    #     # 刷新拨号状态
    #     self._load_dial_status()
    #     # 如果当前在聊天记录标签页，也刷新聊天记录
    #     if self.tabs.currentIndex() == 1:  # 聊天记录标签页索引为1
    #         self._load_chat_history()

    def closeEvent(self, event):
        """关闭对话框时停止定时器"""
        if hasattr(self, 'refresh_timer'):
            self.refresh_timer.stop()
        super().closeEvent(event)

    def _create_info_tab(self):
        """创建基本信息标签页"""
        tab = QtWidgets.QWidget()
        layout = create_v_layout(tab, margins=(16, 16, 16, 16), spacing=16)

        # 设备基本信息表单
        form = QtWidgets.QFormLayout()
        form.setSpacing(12)

        self.mic_edit = QtWidgets.QLineEdit(self.device_card.mic_edit.text())
        self.speaker_edit = QtWidgets.QLineEdit(self.device_card.speaker_edit.text())
        self.phone_edit = QtWidgets.QLineEdit(self.device_card.phone_edit.text())
        form.addRow("麦克风", self.mic_edit)
        form.addRow("扬声器", self.speaker_edit)
        form.addRow("手机号", self.phone_edit)

        layout.addLayout(form)

        # 分隔线
        line = QtWidgets.QFrame()
        line.setFrameShape(QtWidgets.QFrame.HLine)
        line.setStyleSheet(f"color: {COLORS['border']};")
        layout.addWidget(line)

        # 设备连接状态
        status_label = QtWidgets.QLabel("设备状态")
        status_label.setStyleSheet("font-weight: 600; font-size: 14px;")
        layout.addWidget(status_label)

        status = "在线" if self.device_card.is_online else "离线"
        if self.device_card.connected_at:
            status += f" · {self.device_card.connected_at.strftime('%H:%M:%S')} 接入"
        self.status_label = QtWidgets.QLabel(status)
        self.status_label.setProperty("secondary", "true")
        layout.addWidget(self.status_label)

        # 分隔线
        line2 = QtWidgets.QFrame()
        line2.setFrameShape(QtWidgets.QFrame.HLine)
        line2.setStyleSheet(f"color: {COLORS['border']};")
        layout.addWidget(line2)

        # 拨号状态区域（带刷新按钮）
        dial_header = create_h_layout(spacing=10)
        dial_label = QtWidgets.QLabel("拨号状态")
        dial_label.setStyleSheet("font-weight: 600; font-size: 14px;")
        dial_header.addWidget(dial_label)
        dial_header.addStretch()
        
        self.refresh_dial_btn = QtWidgets.QPushButton("刷新")
        apply_button_style(self.refresh_dial_btn, "tertiary")
        self.refresh_dial_btn.clicked.connect(self._load_dial_status)
        dial_header.addWidget(self.refresh_dial_btn)


        layout.addLayout(dial_header)

        # 拨号状态显示区域
        dial_status_layout = create_v_layout(spacing=8)
        #设置当前拨号客户信息
        self.dial_customer_label = QtWidgets.QLabel("拨号客户：无")
        self.dial_customer_label.setProperty("secondary", "true")
        dial_status_layout.addWidget(self.dial_customer_label)

        # 当前拨号手机号
        self.dial_phone_label = QtWidgets.QLabel("当前拨号：无")
        self.dial_phone_label.setProperty("secondary", "true")
        dial_status_layout.addWidget(self.dial_phone_label)

        # 拨号状态
        self.dial_status_label = QtWidgets.QLabel("状态：未拨号")
        self.dial_status_label.setProperty("secondary", "true")
        dial_status_layout.addWidget(self.dial_status_label)

        # 拨号时间
        self.dial_time_label = QtWidgets.QLabel("拨号时间：-")
        self.dial_time_label.setProperty("secondary", "true")
        dial_status_layout.addWidget(self.dial_time_label)

        layout.addLayout(dial_status_layout)

        layout.addStretch()

        return tab

    def _create_chat_tab(self):
        """创建聊天记录标签页"""
        tab = QtWidgets.QWidget()
        layout = create_v_layout(tab, margins=(16, 16, 16, 16), spacing=12)

        # 聊天记录标题栏（带刷新按钮）
        chat_header = create_h_layout(spacing=10)
        chat_label = QtWidgets.QLabel("聊天记录")
        chat_label.setStyleSheet("font-weight: 600; font-size: 14px;")
        chat_header.addWidget(chat_label)
        chat_header.addStretch()
        
        self.refresh_chat_btn = QtWidgets.QPushButton("刷新")
        apply_button_style(self.refresh_chat_btn, "tertiary")
        self.refresh_chat_btn.clicked.connect(self._load_chat_history)
        chat_header.addWidget(self.refresh_chat_btn)
        
        layout.addLayout(chat_header)

        # 聊天记录显示区域（可滚动）
        self.chat_list = QtWidgets.QListWidget()
        self.chat_list.setStyleSheet(f"""
            QListWidget {{
                background-color: #181818;
                border: 1px solid {COLORS['border']};
                border-radius: 8px;
                padding: 8px;
            }}
            QListWidget::item {{
                background-color: {COLORS['card_bg']};
                border: 1px solid {COLORS['border']};
                border-radius: 6px;
                padding: 10px;
                margin-bottom: 8px;
                color: {COLORS['text_primary']};
            }}
            QListWidget::item:hover {{
                background-color: rgba(59, 130, 246, 0.1);
            }}
        """)
        layout.addWidget(self.chat_list, 1)  # 占据剩余空间
        return tab

    def _load_dial_status(self):
        """加载拨号状态（业务逻辑待实现）"""
        # TODO: 实现获取当前拨号状态的业务逻辑
        customer_data = get_customer_data(self.phone_edit.text().strip())
        dial_phone = customer_data.get("phone","")
        dial_status = get_talking_status(self.phone_edit.text().strip())
        if dial_phone:
            self.dial_customer_label.setText(f"客户 姓名：{customer_data.get('customer_name')} 年龄：{customer_data.get('age')}")
            self.dial_phone_label.setText(f"当前拨号：{dial_phone}")
            self.dial_status_label.setText(f"状态：{dial_status}")
        else:
            self.dial_phone_label.setText("当前拨号：无")
            self.dial_status_label.setText("状态：未拨号")

    def _load_chat_history(self):
        """加载聊天记录"""
        try:
            phone = self.phone_edit.text().strip()
            if not phone:
                return
            
            customer_data = get_customer_data(phone)
            called_phone = customer_data.get("phone", "")
            if not called_phone:
                self.chat_list.clear()
                return
            
            # 获取聊天记录数据
            chat_data = get_chat_data(device_phone=phone, called_phone=called_phone)
            
            # 如果数据为空或None，清空列表
            if not chat_data:
                self.chat_list.clear()
                return
            
            # 处理数据：可能是字符串列表或字典列表
            chat_records = []

            if isinstance(chat_data, list):
                # 如果是列表，直接使用
                chat_records = chat_data
            else:
                # 其他类型，尝试转换为列表
                chat_records = []
            
            # 按时间戳排序（从旧到新）
            try:
                chat_records.sort(key=lambda x: x.get('timestamp', ''))
            except:
                pass
            
            self.chat_list.clear()
            
            for record in chat_records:
                try:
                    # 解析记录字段
                    sender = record.get('sender', '')
                    receiver = record.get('receiver', '')
                    content = record.get('content', '')
                    timestamp = record.get('timestamp', '')
                    
                    # 判断消息类型：如果sender是当前设备手机号，则是发送；否则是接收
                    if sender == phone:
                        msg_type = "发送"
                        msg_type_color = COLORS['primary_orange']
                    else:
                        msg_type = "接收"
                        msg_type_color = COLORS['success']
                    
                    # 格式化时间戳
                    time_str = self._format_timestamp(timestamp)
                    
                    # 创建列表项
                    item = QtWidgets.QListWidgetItem()
                    
                    # 格式化显示文本：更清晰的格式
                    # 发送消息：📤 [时间] 发送: 内容
                    # 接收消息：📥 [时间] 接收: 内容
                    icon = "📤" if msg_type == "发送" else "📥"
                    display_text = f"{icon} [{time_str}] {msg_type}: {content}"
                    item.setText(display_text)
                    
                    # 设置文本颜色区分发送和接收
                    item.setForeground(QtGui.QColor(msg_type_color))
                    
                    # 设置工具提示（鼠标悬停时显示完整内容）
                    item.setToolTip(f"时间: {timestamp}\n类型: {msg_type}\n内容: {content}")
                    
                    # 设置项的高度（如果内容较长，自动调整）
                    item.setSizeHint(QtCore.QSize(item.sizeHint().width(), max(40, len(content) // 30 * 20 + 20)))
                    
                    self.chat_list.addItem(item)
                except Exception as e:
                    # 跳过解析失败的记录
                    continue
            
            # 自动滚动到底部显示最新消息
            if self.chat_list.count() > 0:
                self.chat_list.scrollToBottom()
        except Exception as e:
            # 出错时清空列表
            self.chat_list.clear()
            print(f"加载聊天记录失败: {e}")
    
    def _format_timestamp(self, timestamp: str) -> str:
        """格式化时间戳为可读格式"""
        if not timestamp:
            return "-"
        try:
            # 解析ISO格式时间戳：2025-11-19T06:40:26.621373
            if 'T' in timestamp:
                dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
            else:
                # 尝试其他格式
                dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")
            # 格式化为：11-19 06:40:26
            return dt.strftime("%m-%d %H:%M:%S")
        except Exception:
            # 如果解析失败，返回原始字符串
            return timestamp[:16] if len(timestamp) > 16 else timestamp



    def _test_device(self):
        """测试设备（业务逻辑待实现）"""
        # TODO: 实现测试设备的业务逻辑
        # CustomMessageBox("提示", "已向设备发送测试指令。", "info", self).exec_()
        pass

    def _apply_changes(self):
        """应用配置修改"""
        self.device_card.update_fields(
            self.mic_edit.text().strip(),
            self.speaker_edit.text().strip(),
            self.phone_edit.text().strip()
        )
        self.accept()


class StatusOverview(QtWidgets.QWidget):
    """设备状态概览"""

    def __init__(self, parent=None):
        super().__init__(parent)
        layout = create_h_layout(spacing=12)
        self._values: Dict[str, QtWidgets.QLabel] = {}
        specs = [
            ("total", "总设备", "🗂", COLORS['text_primary']),
            ("online", "在线设备", "📶", COLORS['success']),
            ("offline", "离线设备", "⛔", COLORS['error']),
        ]
        for key, title, icon_text, accent in specs:
            card, value_label = self._create_card(title, icon_text, accent)
            self._values[key] = value_label
            layout.addWidget(card)
        self.setLayout(layout)

    def _create_card(self, title: str, icon_text: str, accent: str):
        card = QtWidgets.QWidget()
        card.setObjectName("status_card")
        inner = create_v_layout(card, spacing=4)
        icon_label = QtWidgets.QLabel(icon_text)
        icon_label.setStyleSheet("font-size: 20px;")
        title_label = QtWidgets.QLabel(title)
        title_label.setProperty("secondary", "true")
        value_label = QtWidgets.QLabel("0")
        value_label.setStyleSheet(f"font-size: 28px; font-weight: 600; color: {accent};")
        inner.addWidget(icon_label)
        inner.addWidget(title_label)
        inner.addWidget(value_label)
        return card, value_label

    def update_stats(self, total: int, online: int, offline: int):
        self._values["total"].setText(str(total))
        self._values["online"].setText(str(online))
        self._values["offline"].setText(str(offline))


class ConfigManagementDialog(QtWidgets.QDialog):
    """配置管理弹窗"""

    def __init__(self, devices: List[Dict], parent=None):
        super().__init__(parent)
        self.setModal(True)
        self.setWindowTitle("配置管理")
        self.setFixedSize(600, 400)
        self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
        self.setAttribute(Qt.WA_TranslucentBackground, True)

        shadow = QtWidgets.QGraphicsDropShadowEffect()
        shadow.setColor(QtGui.QColor(0, 0, 0, 100))
        shadow.setBlurRadius(15)
        shadow.setOffset(0, 5)

        container = QtWidgets.QWidget()
        container.setStyleSheet(f"""
            QWidget {{
                background-color: {COLORS['background']};
                border: 1px solid {COLORS['border']};
                border-radius: 12px;
            }}
        """)
        container.setGraphicsEffect(shadow)

        wrapper = create_v_layout(self, margins=(0, 0, 0, 0))
        wrapper.addWidget(container)

        layout = create_v_layout(container, margins=(20, 20, 20, 20), spacing=15)

        title = QtWidgets.QLabel("配置管理")
        title.setStyleSheet("font-size: 18px; font-weight: 700;")
        layout.addWidget(title)

        self.config_tabs = QtWidgets.QTabWidget()
        self.config_tabs.setStyleSheet(f"""
            QTabWidget::pane {{
                background-color: {COLORS['card_bg']};
                border: 1px solid {COLORS['border']};
                border-radius: 8px;
            }}
            QTabBar::tab {{
                background-color: transparent;
                color: {COLORS['text_secondary']};
                padding: 8px 20px;
                border-radius: 6px;
                margin-right: 6px;
            }}
            QTabBar::tab:selected {{
                color: {COLORS['text_primary']};
                border-bottom: 2px solid {COLORS['primary_orange']};
            }}
        """)
        layout.addWidget(self.config_tabs, 1)

        self.device_tab = QtWidgets.QWidget()
        device_layout = create_v_layout(self.device_tab, margins=(15, 15, 15, 15), spacing=12)
        self.device_search = QtWidgets.QLineEdit()
        self.device_search.setPlaceholderText("搜索设备配置...")
        device_layout.addWidget(self.device_search)

        self.device_list = QtWidgets.QListWidget()
        self.device_list.setStyleSheet(f"""
            QListWidget {{
                background-color: #181818;
                border: 1px solid {COLORS['border']};
                border-radius: 8px;
                padding: 8px;
            }}
            QListWidget::item {{
                height: 38px;
                border-radius: 6px;
                padding: 0 10px;
                margin-bottom: 4px;
                color: {COLORS['text_secondary']};
            }}
            QListWidget::item:selected {{
                background-color: rgba(59, 130, 246, 0.2);
                color: {COLORS['text_primary']};
            }}
            QListWidget::item:hover {{
                background-color: rgba(255,255,255,0.05);
            }}
        """)
        device_layout.addWidget(self.device_list, 1)
        self.config_tabs.addTab(self.device_tab, "设备列表")

        self.global_tab = QtWidgets.QWidget()
        global_layout = create_v_layout(self.global_tab, margins=(15, 15, 15, 15), spacing=12)
        global_layout.addWidget(QtWidgets.QLabel("全局设置开发中..."))
        self.config_tabs.addTab(self.global_tab, "全局设置")

        footer = create_h_layout(spacing=10)
        self.btn_import = create_config_button("导入配置", "import", "secondary", "导入配置")
        self.btn_export = create_config_button("导出全部", "export", "secondary", "导出全部配置")
        self.btn_reset = create_config_button("恢复默认", "reset", "tertiary", "恢复默认配置")
        self.btn_confirm = create_config_button("确定", "save", "primary", "关闭弹窗")
        add_widgets_to_layout(footer, [self.btn_import, self.btn_export, self.btn_reset, self.btn_confirm])
        footer.addStretch()
        layout.addLayout(footer)

        self.btn_confirm.clicked.connect(self.accept)
        self.device_search.textChanged.connect(self._filter_devices)

        self.devices = devices or []
        self._populate_devices()

    def _populate_devices(self):
        self.device_list.clear()
        for device in self.devices:
            text = f"{device.get('phone', '')} - {device.get('input_name', '')}/{device.get('output_name', '')}"
            item = QtWidgets.QListWidgetItem(text)
            self.device_list.addItem(item)

    def _filter_devices(self, text: str):
        for i in range(self.device_list.count()):
            item = self.device_list.item(i)
            item.setHidden(text.lower() not in item.text().lower())

class QtLogSignal(QtCore.QObject):
    emitted = QtCore.pyqtSignal(str, str, str)  # (device_phone, level, message)


class DeviceDetailWidget(QtWidgets.QWidget):
    """保留占位的旧组件（未使用）"""
    pass
class QtLogHandler(logging.Handler):
    def __init__(self, signal: QtLogSignal):
        super().__init__()
        self.signal = signal

    def emit(self, record: logging.LogRecord):
        try:
            msg = self.format(record)
        except Exception:
            msg = record.getMessage()
        # 获取日志级别（用于着色）
        level = record.levelname.lower()  # debug/info/warning/error/critical
        device_phone = getattr(record, "device_phone", None) or _get_device_context()
        self.signal.emitted.emit(device_phone or "", level, msg)


class LogStream(QtCore.QObject):
    """用于重定向stdout/stderr的流对象"""
    writeSignal = QtCore.pyqtSignal(str, str, str)  # (device_phone, level, message)

    def write(self, s):
        if s and s.strip():
            device_phone = _get_device_context() or ""
            # stdout默认info级别，stderr默认error级别
            level = "error" if sys.stderr == self else "info"
            self.writeSignal.emit(device_phone, level, s.rstrip())

    def flush(self):
        pass


class DeviceManagerThread(QtCore.QThread):
    started_ok = QtCore.pyqtSignal(list)
    error = QtCore.pyqtSignal(str)
    stopped = QtCore.pyqtSignal()
    device_status_changed = QtCore.pyqtSignal(str, bool)  # (phone, is_online)

    def __init__(self, target_devices: List[Dict]):
        super().__init__()
        self.target_devices = target_devices
        self.manager = SimpleMultiDeviceManager()
        self._stop_requested = False

    def run(self):
        devices = []  # 初始化为空列表，确保finally块中可以使用
        try:
            if self.manager.running:
                self.manager.stop_all_devices()
                self.msleep(500)

            devices = self.manager.scan_audio_devices(target_devices=self.target_devices)
            if not devices:
                self.error.emit("未发现可用的目标输入设备，请检查设备连接与名称匹配")
                return
            self.manager.devices = devices
            self.started_ok.emit(devices)
            self.manager.start_all_devices()

            # 模拟设备状态监测（实际可根据manager的设备状态回调修改）
            for device in devices:
                phone = device.get('phone', '')
                if phone:
                    self.device_status_changed.emit(phone, True)  # 标记为在线

            while self.manager.running and not self._stop_requested:
                self.msleep(200)
        except Exception as e:
            self.error.emit(str(e))
        finally:
            try:
                self.manager.stop_all_devices()
            except Exception:
                pass
            
            # 标记所有设备离线 - 使用target_devices确保所有配置的设备都被标记为离线
            # 每个设备单独处理，确保即使某个设备出错也不影响其他设备
            for device in self.target_devices:
                try:
                    phone = device.get('phone', '')
                    if phone:
                        self.device_status_changed.emit(phone, False)
                except Exception:
                        pass  # 忽略单个设备的错误，继续处理其他设备
            
            try:
                self.stopped.emit()
            except Exception:
                pass

    def stop(self):
        """停止设备管理器"""
        self._stop_requested = True
        try:
            if self.manager:
                self.manager.stop_all_devices()
        except Exception:
            pass


class CustomMessageBox(QtWidgets.QDialog):
    """自定义弹窗（替换原生QMessageBox）"""

    def __init__(self, title, content, icon_type="info", parent=None):
        super().__init__(parent)
        self.setWindowTitle(title)
        self.setModal(True)
        # 不设置固定尺寸，让内容自适应
        self.setMinimumSize(200, 50)
        self.setMaximumSize(600, 100)  # 设置最大尺寸避免过大
        self.setStyleSheet(f"""
            QDialog {{
                background-color: {COLORS['card_bg']};
                border: 1px solid {COLORS['border']};
                border-radius: 8px;
            }}
            QLabel {{
                font-family: "Microsoft YaHei UI", "思源黑体";
                font-size: 13px;
                color: {COLORS['text_primary']};
            }}
            QPushButton {{
                background-color: {COLORS['primary_orange']};
                color: white;
                border: none;
                border-radius: 4px;
                padding: 4px 12px;
                font-family: "Microsoft YaHei UI", "思源黑体";
                font-size: 12px;
                font-weight: 500;
                min-height: 24px;
                min-width: 40px;
            }}
            QPushButton:hover {{
                background-color: {COLORS['primary_blue']};
            }}
            QPushButton:pressed {{
                background-color: #1d4ed8;
            }}
        """)
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)

        # 阴影效果（更轻量）
        shadow = QtWidgets.QGraphicsDropShadowEffect()
        shadow.setColor(QtGui.QColor(0, 0, 0, 30))
        shadow.setBlurRadius(10)
        shadow.setOffset(0, 2)
        self.setGraphicsEffect(shadow)

        # 水平布局：图标+文本+按钮
        layout = QtWidgets.QHBoxLayout(self)
        layout.setContentsMargins(10, 8, 10, 8)
        layout.setSpacing(10)

        # 图标（使用Unicode字符，更简单可靠）
        self.icon_label = QtWidgets.QLabel()
        if icon_type == "info":
            icon_text = "ℹ"
            icon_color = COLORS['primary_orange']
        elif icon_type == "warning":
            icon_text = "⚠"
            icon_color = COLORS['warning']
        elif icon_type == "error":
            icon_text = "✕"
            icon_color = COLORS['error']
        else:
            icon_text = "ℹ"
            icon_color = COLORS['text_secondary']
        
        self.icon_label.setText(icon_text)
        self.icon_label.setStyleSheet(f"color: {icon_color}; font-size: 16px; font-weight: bold;")
        self.icon_label.setFixedWidth(20)
        layout.addWidget(self.icon_label)

        # 内容文本（占据剩余空间，支持换行）
        self.content_label = QtWidgets.QLabel(content)
        self.content_label.setWordWrap(True)  # 允许换行
        self.content_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        self.content_label.setTextFormat(Qt.PlainText)
        # 设置最大宽度，超出后自动换行
        self.content_label.setMaximumWidth(500)
        layout.addWidget(self.content_label, 1)  # 占据剩余空间

        # 按钮（紧凑尺寸）
        btn = QtWidgets.QPushButton("确定")
        btn.clicked.connect(self.close)
        btn.setFixedWidth(50)  # 固定按钮宽度
        layout.addWidget(btn)
        
        # 根据内容调整对话框大小
        self.adjustSize()

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.mouse_press_pos = event.globalPos() - self.frameGeometry().topLeft()
            event.accept()

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton and self.mouse_press_pos:
            self.move(event.globalPos() - self.mouse_press_pos)
            event.accept()


class LoadingWidget(QtWidgets.QWidget):
    """启动加载动画"""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFixedSize(200, 200)
        self.setStyleSheet("""
            QWidget {
                background-color: #1a202c;
                border: 2px solid #2d3748;
                border-radius: 12px;
            }
            QLabel {
                font-family: "Microsoft YaHei UI", "思源黑体";
                font-size: 15px;
                font-weight: 500;
                color: #3b82f6;
                margin-top: 20px;
            }
        """)
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
        # 移除透明背景，使用实色背景

        # 布局
        layout = QtWidgets.QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(10)

        # 加载动画（圆形旋转）
        self.loading_label = QtWidgets.QLabel()
        self.loading_label.setFixedSize(60, 60)
        self.loading_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage.fromData(b"""
            <svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 24 24">
                <circle cx="12" cy="12" r="10" fill="none" stroke="#4299e1" stroke-width="2" stroke-dasharray="50" stroke-dashoffset="0" stroke-linecap="round"/>
            </svg>
        """))
        self.loading_label.setPixmap(self.loading_pixmap)
        layout.addWidget(self.loading_label, alignment=Qt.AlignCenter)

        # 加载文本
        self.text_label = QtWidgets.QLabel("启动中...")
        self.text_label.setAlignment(Qt.AlignCenter)
        layout.addWidget(self.text_label)

        # 旋转动画
        self.animation = QPropertyAnimation(self.loading_label, b"rotation")
        self.animation.setDuration(1000)
        self.animation.setStartValue(0)
        self.animation.setEndValue(360)
        self.animation.setEasingCurve(QEasingCurve.Linear)
        self.animation.setLoopCount(-1)  # 无限循环

    def showEvent(self, event):
        self.animation.start()
        super().showEvent(event)

    def hideEvent(self, event):
        self.animation.stop()
        super().hideEvent(event)

class LogWidget(QtWidgets.QWidget):
    """带清空/导出功能的日志组件"""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setStyleSheet("background-color: #1a202c;")

        layout = create_v_layout(self, margins=(0, 0, 0, 0), spacing=5)

        btn_layout = create_h_layout(spacing=4)
        btn_layout.addStretch()

        self.clear_btn = QtWidgets.QPushButton("清空")
        self.export_btn = QtWidgets.QPushButton("导出")
        apply_button_style(self.clear_btn, "secondary")
        apply_button_style(self.export_btn, "secondary")

        add_widgets_to_layout(btn_layout, [self.clear_btn, self.export_btn])
        layout.addLayout(btn_layout)

        self.log_edit = QtWidgets.QPlainTextEdit()
        self.log_edit.setReadOnly(True)
        self.log_edit.setStyleSheet(get_log_text_style())
        layout.addWidget(self.log_edit)

        # 绑定按钮事件
        self.clear_btn.clicked.connect(self.log_edit.clear)
        self.export_btn.clicked.connect(self._export_log)

    def append_log(self, text: str, level: str = "info"):
        """追加日志（根据级别着色）"""
        tag_styles = {
            "error": ("#f87171", "#3f0d0d"),
            "critical": ("#f87171", "#3f0d0d"),
            "warning": ("#fbbf24", "#3f2d0d"),
            "info": ("#60a5fa", "#10233d"),
            "debug": ("#a78bfa", "#221034"),
        }
        fg, bg = tag_styles.get(level, ("#9ca3af", "#2a2a2a"))
        cursor = self.log_edit.textCursor()
        cursor.movePosition(QtGui.QTextCursor.End)
        label_format = QtGui.QTextCharFormat()
        label_format.setForeground(QColor(fg))
        label_format.setBackground(QColor(bg))
        label_format.setFontWeight(QtGui.QFont.Bold)
        cursor.insertText(f"[{level.upper()}] ", label_format)

        timestamp_format = QtGui.QTextCharFormat()
        timestamp_format.setForeground(QColor("#888888"))
        cursor.insertText(f"{datetime.now().strftime('%H:%M:%S')} ", timestamp_format)

        message_format = QtGui.QTextCharFormat()
        message_format.setForeground(QColor(COLORS['text_primary']))
        cursor.insertText(text + "\n", message_format)
        self.log_edit.setTextCursor(cursor)
        self.log_edit.verticalScrollBar().setValue(self.log_edit.verticalScrollBar().maximum())

    def _export_log(self):
        """导出日志到txt文件"""
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        path, _ = QtWidgets.QFileDialog.getSaveFileName(
            self, "导出日志", f"日志_{timestamp}.txt", "Text Files (*.txt)"
        )
        if not path:
            return
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(self.log_edit.toPlainText())
            CustomMessageBox("成功", "日志导出完成", "info", self).exec_()
        except Exception as e:
            CustomMessageBox("错误", f"日志导出失败：{e}", "error", self).exec_()



class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        icon_path = get_resource_path("1.ico")
        if os.path.exists(icon_path):
            self.setWindowIcon(QtGui.QIcon(icon_path))
        self.setWindowTitle("多设备语音监测（意数未来）")
        self.resize(1200, 800)
        self.setMinimumSize(900, 600)
        self.worker: DeviceManagerThread = None
        self.config_manager = get_config_manager()

        self.setWindowFlags(Qt.Window | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint)
        self.setStyleSheet(get_global_styles())

        central_widget = QtWidgets.QWidget()
        self.setCentralWidget(central_widget)
        main_layout = create_v_layout(central_widget, margins=(24, 24, 24, 24), spacing=24)

        self.tabs = QtWidgets.QTabWidget()
        main_layout.addWidget(self.tabs)

        # 设备管理标签页（添加图标）
        self.device_tab = QtWidgets.QWidget()
        device_icon = QtGui.QIcon.fromTheme("devices")
        # 自定义标签图标（矢量图）
        device_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage.fromData(b"""
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M23 7l-7 5 7 5V7z"/>
                <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
            </svg>
        """))
        self.tabs.addTab(self.device_tab, QtGui.QIcon(device_pixmap), "设备管理")
        device_layout = create_v_layout(self.device_tab, margins=(0, 0, 0, 0), spacing=20)

        self.status_overview = StatusOverview()
        device_layout.addWidget(self.status_overview)

        controls = create_h_layout(spacing=10)
        self.btn_start = QtWidgets.QPushButton("启动全部")
        self.btn_stop = QtWidgets.QPushButton("停止全部")
        self.btn_add = QtWidgets.QPushButton("添加设备")
        self.btn_remove = QtWidgets.QPushButton("删除选中")
        self.btn_load = QtWidgets.QPushButton("加载配置")
        self.btn_save = QtWidgets.QPushButton("保存配置")
        apply_button_style(self.btn_start, "primary")
        apply_button_style(self.btn_stop, "danger")
        apply_button_style(self.btn_add, "tertiary")
        apply_button_style(self.btn_remove, "secondary")
        apply_button_style(self.btn_load, "secondary")
        apply_button_style(self.btn_save, "primary")
        self.btn_stop.setEnabled(False)
        add_widgets_to_layout(controls, [
            self.btn_start,
            self.btn_stop,
            self.btn_add,
            self.btn_remove,
            self.btn_load,
            self.btn_save,
            "stretch"
        ])
        device_layout.addLayout(controls)

        self.device_scroll_area = QtWidgets.QScrollArea()
        self.device_scroll_area.setWidgetResizable(True)
        self.device_scroll_area.setStyleSheet("border: none;")

        self.device_container = QtWidgets.QWidget()
        self.device_cards_layout = create_v_layout(margins=(0, 0, 0, 0), spacing=12)
        self.device_cards_layout.addStretch()
        self.device_container.setLayout(self.device_cards_layout)
        self.device_scroll_area.setWidget(self.device_container)
        device_layout.addWidget(self.device_scroll_area, 1)

        self.device_cards: List[DeviceCard] = []

        self.logs_tab = QtWidgets.QWidget()
        log_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage.fromData(b"""
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M3 3h18v4H3z"/>
                <path d="M3 11h18v4H3z"/>
                <path d="M3 19h18v2H3z"/>
            </svg>
        """))
        self.tabs.addTab(self.logs_tab, QtGui.QIcon(log_pixmap), "日志监控")
        logs_layout = create_v_layout(self.logs_tab, margins=(0, 0, 0, 0), spacing=10)

        self.device_log_tabs = QtWidgets.QTabWidget()
        self.device_log_tabs.setTabsClosable(False)
        logs_layout.addWidget(self.device_log_tabs)

        self.device_logs: Dict[str, LogWidget] = {}
        self.thread_device_map: Dict[int, str] = {}

        self.general_log = LogWidget()
        self.device_log_tabs.addTab(self.general_log, "通用日志")

        # 配置管理标签页（添加图标）
        self.config_tab = QtWidgets.QWidget()
        config_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage.fromData(b"""
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
                <circle cx="12" cy="12" r="3"/>
            </svg>
        """))
        self.tabs.addTab(self.config_tab, QtGui.QIcon(config_pixmap), "配置管理")
        self._setup_config_tab()

        # Logging setup（修改为支持级别着色）
        self.log_signal = QtLogSignal()
        self.log_signal.emitted.connect(self._append_device_log)
        handler = QtLogHandler(self.log_signal)
        handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", "%H:%M:%S"))
        logging.getLogger().addHandler(handler)
        logging.getLogger().setLevel(logging.INFO)

        # Redirect stdout/stderr to GUI log
        self._stdout = LogStream()
        self._stderr = LogStream()
        self._stdout.writeSignal.connect(self._append_device_log)
        self._stderr.writeSignal.connect(self._append_device_log)
        sys.stdout = self._stdout
        sys.stderr = self._stderr

        # Wire buttons
        self.btn_add.clicked.connect(self._add_row)
        self.btn_remove.clicked.connect(self._remove_selected)
        self.btn_load.clicked.connect(self._load_config)
        self.btn_save.clicked.connect(self._save_config)
        self.btn_start.clicked.connect(self._start)
        self.btn_stop.clicked.connect(self._stop)

        # Defaults
        self._add_row_with_values("789_in", "789_out", "13635518734")
        self._add_row_with_values("1_in", "1_out", "13956915894")

        # 加载配置到界面（静默加载）
        try:
            self._load_config_to_ui(silent=True)
        except Exception as e:
            self._append_log(f"初始化时加载配置失败: {e}", "error")

        # 窗口大小记忆（加载上次保存的位置和大小）
        self._load_window_geometry()

        # 可选：毛玻璃效果（Win10+）
        self.set_glass_effect()



    def set_glass_effect(self):
        """设置毛玻璃效果（Win10+）"""
        if sys.platform == "win32":
            try:
                from ctypes import windll, byref, c_int, c_ulong
                hwnd = int(self.winId())
                windll.dwmapi.DwmSetWindowAttribute(hwnd, 20, byref(c_int(2)), 4)
                windll.dwmapi.DwmSetWindowAttribute(hwnd, 35, byref(c_int(0x4299e1)), 4)
            except Exception as e:
                self._append_log(f"设置毛玻璃效果失败: {e}", "warning")

    def _save_window_geometry(self):
        """保存窗口位置和大小到配置文件"""
        config = {
            "pos_x": self.pos().x(),
            "pos_y": self.pos().y(),
            "width": self.width(),
            "height": self.height()
        }
        try:
            with open("window_geometry.json", "w", encoding="utf-8") as f:
                json.dump(config, f)
        except Exception:
            pass

    def _load_window_geometry(self):
        """加载上次保存的窗口位置和大小"""
        try:
            with open("window_geometry.json", "r", encoding="utf-8") as f:
                config = json.load(f)
            self.move(config["pos_x"], config["pos_y"])
            self.resize(config["width"], config["height"])
        except Exception:
            pass

    def closeEvent(self, event):
        """关闭窗口时保存配置"""
        self._save_window_geometry()
        # 停止worker
        if self.worker and self.worker.isRunning():
            self.worker.stop()
            self.worker.wait(2000)
        event.accept()

    # UI helpers
    def _append_log(self, text: str, level: str = "info"):
        """向后兼容的日志追加方法"""
        self._append_device_log("", level, text)

    def _get_all_device_phones(self) -> List[str]:
        """获取所有配置的设备手机号列表"""
        phones = []
        for card in self.device_cards:
            phone = card.get_data()["phone"]
            if phone:
                phones.append(phone)
        return phones

    def _extract_device_phone_from_text(self, text: str) -> Optional[str]:
        """从日志文本中提取设备手机号"""
        known_phones = self._get_all_device_phones()
        patterns = [
            r'设备\s+(\d{4,11})',
            r'设备\s+(\d{4,11})\s+\(',
            r'设备\s+(\d{4,11})\s+session_id',
            r'设备\s+(\d{4,11})\s+麦克风',
            r'设备\s+(\d{4,11})\s+检测到',
        ]
        for pattern in patterns:
            match = re.search(pattern, text)
            if match:
                phone = match.group(1)
                if phone in known_phones or phone in self.device_logs:
                    return phone
        return None

    def _append_device_log(self, device_phone: str, level: str, text: str):
        """追加设备日志（支持级别着色）"""
        # 提取设备手机号
        if not device_phone:
            device_phone = self._extract_device_phone_from_text(text)
            if device_phone:
                thread_id = threading.get_ident()
                self.thread_device_map[thread_id] = device_phone

        if not device_phone:
            thread_id = threading.get_ident()
            device_phone = self.thread_device_map.get(thread_id)

        if device_phone:
            if device_phone not in self.device_logs:
                self._ensure_device_log_tab(device_phone)
            log_widget = self.device_logs[device_phone]
            log_widget.append_log(text, level)
        else:
            self.general_log.append_log(text, level)

    def _ensure_device_log_tab(self, device_phone: str, device_name: str = ""):
        """确保设备日志标签页存在"""
        if device_phone not in self.device_logs:
            log_widget = LogWidget()
            tab_label = f"{device_phone}" + (f" ({device_name})" if device_name else "")
            self.device_log_tabs.addTab(log_widget, tab_label)
            self.device_logs[device_phone] = log_widget

    def _remove_device_log_tab(self, device_phone: str):
        """移除设备日志标签页"""
        if device_phone in self.device_logs:
            log_widget = self.device_logs[device_phone]
            for i in range(self.device_log_tabs.count()):
                if self.device_log_tabs.widget(i) == log_widget:
                    self.device_log_tabs.removeTab(i)
                    break
            del self.device_logs[device_phone]

    def _update_device_status(self, phone: str, is_online: bool):
        """更新设备在线状态"""
        for card in self.device_cards:
            if card.matches_phone(phone):
                card.set_status(is_online)
                break
        self._update_overview_counts()
    
    def _stop_device_ai(self, device_phone: str):
        """停止指定设备的AI回复（断开WS连接）"""
        if not self.worker or not self.worker.isRunning():
            CustomMessageBox("提示", "设备管理器未运行，无法停止AI", "warning", self).exec_()
            return
        
        try:
            success = self.worker.manager.disconnect_device_ws(device_phone)
            if success:
                self._append_log(f"设备 {device_phone} AI回复已停止", "info")
                CustomMessageBox("成功", f"设备 {device_phone} 的AI回复已停止", "info", self).exec_()
            else:
                CustomMessageBox("失败", f"设备 {device_phone} 未找到或未连接", "warning", self).exec_()
        except Exception as e:
            self._append_log(f"停止设备 {device_phone} AI失败: {e}", "error")
            CustomMessageBox("错误", f"停止AI失败: {e}", "error", self).exec_()

    def _validate_phone(self, phone: str) -> bool:
        """验证手机号格式（11位数字）"""
        return re.match(r'^\d{11}$', phone.strip()) is not None

    def _add_device_card(self, input_name: str, output_name: str, phone: str):
        card = DeviceCard({"input_name": input_name, "output_name": output_name, "phone": phone})
        card.detail_requested.connect(self._open_device_detail)
        card.stop_ai_requested.connect(self._stop_device_ai)
        self.device_cards.append(card)
        self.device_cards_layout.insertWidget(self.device_cards_layout.count() - 1, card)
        self._update_overview_counts()
        return card

    def _add_row_with_values(self, input_name: str, output_name: str, phone: str):
        self._add_device_card(input_name, output_name, phone)

    def _clear_device_cards(self):
        for card in self.device_cards:
            card.setParent(None)
            card.deleteLater()
        self.device_cards.clear()
        self._update_overview_counts()

    def _open_device_detail(self, card: DeviceCard):
        dialog = DeviceDetailDialog(card, self)
        dialog.exec_()

    def _update_overview_counts(self):
        total = len(self.device_cards)
        online = sum(1 for card in self.device_cards if card.is_online)
        offline = max(total - online, 0)
        self.status_overview.update_stats(total, online, offline)

    def _add_row(self):
        self._add_device_card("", "", "")

    def _remove_selected(self):
        removed = False
        for card in list(self.device_cards):
            if card.is_selected():
                self.device_cards.remove(card)
                card.setParent(None)
                card.deleteLater()
                removed = True
        if removed:
            self._update_overview_counts()

    def _collect_devices(self) -> List[Dict]:
        """收集设备配置（含手机号验证）"""
        devices = []
        invalid_phones = []
        empty_devices = []  # 记录缺少必要信息的设备
        
        for card in self.device_cards:
            data = card.get_data()
            input_name = data["input_name"]
            output_name = data["output_name"]
            phone = data["phone"]

            # 检查必要字段是否为空
            if not input_name or not output_name or not phone:
                empty_devices.append(phone or "未设置手机号")
                continue

            # 验证手机号格式
            if not self._validate_phone(phone):
                invalid_phones.append(phone)
                continue

            devices.append({
                "input_name": input_name,
                "output_name": output_name,
                "phone": phone,
            })

        # 统一提示错误信息（只显示一次）
        error_messages = []
        if invalid_phones:
            error_messages.append(f"以下手机号格式无效（需11位数字）：{', '.join(invalid_phones)}")
        if empty_devices:
            error_messages.append(f"以下设备缺少必要信息（麦克风、扬声器或手机号）：{', '.join(empty_devices[:3])}")  # 最多显示3个
        
        # 如果有任何验证失败，显示错误并返回空列表（阻止启动）
        if error_messages:
            error_msg = "\n".join(error_messages)
            CustomMessageBox("警告", error_msg, "warning", self).exec_()
            return []  # 验证失败，返回空列表，阻止启动

        # 如果没有有效设备，提示用户
        if not devices:
            CustomMessageBox("警告", "请至少配置一条有效的设备映射（手机号需11位数字）", "warning", self).exec_()
            return []

        return devices

    def _load_config(self):
        path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "加载配置", filter="JSON (*.json)")
        if not path:
            return
        try:
            with open(path, "r", encoding="utf-8") as f:
                data = json.load(f)
            if not isinstance(data, list):
                raise ValueError("配置文件格式错误：应为列表")
            self._clear_device_cards()
            for item in data:
                self._add_device_card(item.get("input_name", ""), item.get("output_name", ""), item.get("phone", ""))
            self._append_log(f"已加载配置: {path}")
        except Exception as e:
            CustomMessageBox("错误", f"加载失败: {e}", "error", self).exec_()

    def _save_config(self):
        path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "保存配置", filter="JSON (*.json)",
                                                        options=QtWidgets.QFileDialog.DontConfirmOverwrite)
        if not path:
            return
        try:
            data = self._collect_devices()
            with open(path, "w", encoding="utf-8") as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
            self._append_log(f"已保存配置: {path}")
        except Exception as e:
            CustomMessageBox("错误", f"保存失败: {e}", "error", self).exec_()

    def _set_running_ui(self, running: bool):
        """设置运行状态的UI"""
        for w in [self.btn_add, self.btn_remove, self.btn_load, self.btn_save]:
            w.setEnabled(not running)
        self.btn_start.setEnabled(not running)
        self.btn_stop.setEnabled(running)
        for card in self.device_cards:
            card.set_editable(not running)
            # 只有在运行时才能停止AI
            card.stop_ai_btn.setEnabled(running)

        # 启动按钮加载动画
        if running:
            self.btn_start.setText("启动中...")
        else:
            self.btn_start.setText("启动全部")

    def _start(self):
        """启动监测（添加加载动画和验证）"""
        # 先验证设备配置，验证通过后再显示加载动画
        target_devices = self._collect_devices()
        if not target_devices:
            # 验证失败，_collect_devices已经显示了错误提示，直接返回
            # 不显示加载动画，不启动worker
            return

        # 验证通过，显示启动加载动画
        loading_widget = LoadingWidget(self)
        loading_widget.move(
            self.frameGeometry().center().x() - loading_widget.width() // 2,
            self.frameGeometry().center().y() - loading_widget.height() // 2
        )
        loading_widget.show()
        QtWidgets.QApplication.processEvents()  # 刷新UI

        # 停止旧worker
        if self.worker and self.worker.isRunning():
            self._append_log("检测到已有worker在运行，正在停止...")
            self.worker.stop()
            if self.worker.wait(5000):
                self._append_log("旧worker已停止")
            else:
                self._append_log("警告: 旧worker未在5秒内停止，强制继续", "warning")
                try:
                    self.worker.started_ok.disconnect()
                    self.worker.error.disconnect()
                    self.worker.stopped.disconnect()
                    self.worker.device_status_changed.disconnect()
                except:
                    pass

        # 创建新worker
        self.worker = DeviceManagerThread(target_devices)
        self.worker.started_ok.connect(self._on_started)
        self.worker.error.connect(self._on_error)
        self.worker.stopped.connect(self._on_stopped)
        self.worker.device_status_changed.connect(self._update_device_status)
        self.worker.start()
        self._set_running_ui(True)

        # 隐藏加载动画
        loading_widget.hide()

    def _stop(self):
        """停止监测"""
        if self.worker:
            self._append_log("正在停止设备监测……")
            self.worker.stop()
            if self.worker.wait(5000):
                self._append_log("设备监测已完全停止")
            else:
                self._append_log("警告: worker未在5秒内停止", "warning")
            
            # 手动将所有设备标记为离线（确保状态更新，即使信号丢失）
            for card in self.device_cards:
                if card.is_online:
                    card.set_status(False)
            self._update_overview_counts()
            
            try:
                self.worker.started_ok.disconnect()
                self.worker.error.disconnect()
                self.worker.stopped.disconnect()
                self.worker.device_status_changed.disconnect()
            except:
                pass
            self.worker = None
            self._set_running_ui(False)

    def _on_started(self, devices: list):
        """设备启动成功回调"""
        for device in devices:
            device_phone = device.get('phone', '')
            device_name = device.get('name', '')
            if device_phone:
                self._ensure_device_log_tab(device_phone, device_name)

    def _on_error(self, msg: str):
        """处理worker错误"""
        self._append_log(f"错误: {msg}", "error")
        self._set_running_ui(False)
        # 显示错误弹窗（这是真正的运行时错误，需要用户知道）
        CustomMessageBox("错误", msg, "error", self).exec_()

    def _on_stopped(self):
        self._append_log("监测已停止")
        self._set_running_ui(False)

    def _update_speaker_list(self):
        """根据版本选择更新说话人列表"""
        current_speaker = self.speaker.currentText()
        self.speaker.clear()
        if hasattr(self, 'speakers') and self.speakers:
            if self.asr_model.currentText() == "男生":
                self.speaker.addItems(self.speakers.get("boys", []))
            else:
                self.speaker.addItems(self.speakers.get("girls", []))
        index = self.speaker.findText(current_speaker)
        if index >= 0:
            self.speaker.setCurrentIndex(index)

    def _update_custom_speaker_visibility(self):
        """根据版本选择更新自定义说话人输入的可见性"""
        is_sc_version = self.asr_model.currentText() == "SC"
        self.custom_speaker_input.setVisible(is_sc_version)
        self.add_speaker_btn.setVisible(is_sc_version)

    def _add_custom_speaker(self):
        """添加自定义说话人到SC版本列表"""
        custom_speaker = self.custom_speaker_input.text().strip()
        if not custom_speaker:
            CustomMessageBox("警告", "请输入说话人名称", "warning", self).exec_()
            return

        existing_items = [self.speaker.itemText(i) for i in range(self.speaker.count())]
        if custom_speaker in existing_items:
            CustomMessageBox("提示", "说话人已存在", "info", self).exec_()
            return

        self.speaker.addItem(custom_speaker)
        self.speaker.setCurrentText(custom_speaker)
        self.custom_speaker_input.clear()
        self._append_log(f"已添加自定义说话人: {custom_speaker}")

    def _setup_config_tab(self):
        """设置配置管理标签页（卡片式布局优化）"""
        layout = QtWidgets.QVBoxLayout(self.config_tab)
        layout.setSpacing(15)
        layout.setContentsMargins(0, 0, 0, 0)

        # 顶部按钮区域
        action_bar = create_h_layout(spacing=10)
        self.btn_config_load = create_config_button("加载配置", "load", "secondary", "从本地文件加载设备配置")
        self.btn_config_save = create_config_button("保存配置", "save", "primary", "将当前配置保存到本地")
        self.btn_config_import = create_config_button("导入配置", "import", "secondary", "导入设备配置集合")
        self.btn_config_export = create_config_button("导出全部", "export", "secondary", "导出全部配置为文件")
        self.btn_config_reset = create_config_button("恢复默认", "reset", "tertiary", "恢复默认配置")
        self.btn_config_manage = create_config_button("管理面板", "save", "secondary", "打开配置管理弹窗")

        add_widgets_to_layout(action_bar, [
            self.btn_config_load,
            self.btn_config_save,
            self.btn_config_import,
            self.btn_config_export,
            self.btn_config_reset,
            self.btn_config_manage,
            "stretch"
        ])
        layout.addLayout(action_bar)

        # 滚动区域
        scroll = QtWidgets.QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setStyleSheet(f"""
            QScrollArea {{
                background-color: {COLORS['background']};
                border: none;
            }}
        """)
        scroll_widget = QtWidgets.QWidget()
        scroll_widget.setStyleSheet(f"""
            QWidget {{
                background-color: {COLORS['background']};
            }}
        """)
        scroll_layout = QtWidgets.QVBoxLayout(scroll_widget)
        scroll_layout.setSpacing(20)
        scroll_layout.setContentsMargins(0, 0, 0, 0)

        # TTS配置卡片
        tts_card = QtWidgets.QWidget()
        tts_card.setStyleSheet(f"""
            QWidget {{
                background-color: {COLORS['card_bg']};
                border: 1px solid {COLORS['border']};
                border-radius: 10px;
                padding: 16px;
            }}
        """)
        # 添加阴影效果增加立体感
        tts_shadow = QtWidgets.QGraphicsDropShadowEffect()
        tts_shadow.setColor(QtGui.QColor(0, 0, 0, 100))
        tts_shadow.setBlurRadius(10)
        tts_shadow.setOffset(0, 2)
        tts_card.setGraphicsEffect(tts_shadow)
        tts_layout = QtWidgets.QFormLayout(tts_card)
        tts_layout.setSpacing(10)
        tts_layout.setContentsMargins(0, 0, 0, 0)

        # TTS配置标题
        tts_title = QtWidgets.QLabel("TTS配置")
        tts_title.setStyleSheet("""
            QLabel {
                font-size: 14px;
                font-weight: 700;
                color: #e2e8f0;
                margin-bottom: 12px;
                padding-bottom: 8px;
                border-bottom: 1px solid #374151;
            }
        """)
        tts_layout.addRow(tts_title)

        self.asr_model = QtWidgets.QComboBox()
        self.asr_model.addItems(["男生", "女生"])
        tts_layout.addRow("性别:", self.asr_model)

        self.speaker = QtWidgets.QComboBox()
        self.asr_model.currentTextChanged.connect(self._update_speaker_list)
        self._update_speaker_list()
        tts_layout.addRow("说话人:", self.speaker)
        scroll_layout.addWidget(tts_card)

        # Dialog配置卡片
        dialog_card = QtWidgets.QWidget()
        dialog_card.setStyleSheet(f"""
            QWidget {{
                background-color: {COLORS['card_bg']};
                border: 1px solid {COLORS['border']};
                border-radius: 10px;
                padding: 16px;
            }}
        """)
        # 添加阴影效果增加立体感
        dialog_shadow = QtWidgets.QGraphicsDropShadowEffect()
        dialog_shadow.setColor(QtGui.QColor(0, 0, 0, 100))
        dialog_shadow.setBlurRadius(10)
        dialog_shadow.setOffset(0, 2)
        dialog_card.setGraphicsEffect(dialog_shadow)
        dialog_layout = QtWidgets.QFormLayout(dialog_card)
        dialog_layout.setSpacing(10)
        dialog_layout.setContentsMargins(0, 0, 0, 0)

        # Dialog配置标题
        dialog_title = QtWidgets.QLabel("对话配置")
        dialog_title.setStyleSheet("""
            QLabel {
                font-size: 14px;
                font-weight: 700;
                color: #e2e8f0;
                margin-bottom: 12px;
                padding-bottom: 8px;
                border-bottom: 1px solid #374151;
            }
        """)
        dialog_layout.addRow(dialog_title)

        self.bot_name = QtWidgets.QLineEdit()
        dialog_layout.addRow("机器人名称:", self.bot_name)

        self.system_role = QtWidgets.QTextEdit()
        self.system_role.setMinimumHeight(160)
        dialog_layout.addRow("系统角色:", self.system_role)

        self.speaking_style = QtWidgets.QTextEdit()
        self.speaking_style.setMinimumHeight(80)
        dialog_layout.addRow("说话风格:", self.speaking_style)

        self.kcb_content = QtWidgets.QTextEdit()
        self.kcb_content.setMinimumHeight(40)
        dialog_layout.addRow("开场白模板:", self.kcb_content)

        kcb_hint = QtWidgets.QLabel(
            "提示：使用 {greeting} 占位符可自动替换为上午好/下午好/晚上好。如果想传递客户名称，需要在合适位置加{customer_name}占位，下方短信模板一同")
        kcb_hint.setStyleSheet("""
            color: #86efac;
            font-size: 11px;
            font-style: italic;
            padding: 4px 8px;
            background-color: rgba(134, 239, 172, 0.08);
            border-radius: 4px;
        """)
        kcb_hint.setWordWrap(True)
        kcb_hint.setMaximumSize(800, 50)
        dialog_layout.addRow("", kcb_hint)

        self.sms_content = QtWidgets.QTextEdit()
        self.sms_content.setMinimumHeight(40)
        self.sms_content.setMaximumHeight(80)
        dialog_layout.addRow("短信模板:", self.sms_content)

        self.location_city = QtWidgets.QLineEdit()
        dialog_layout.addRow("城市:", self.location_city)

        scroll_layout.addWidget(dialog_card)

        scroll.setWidget(scroll_widget)
        layout.addWidget(scroll)

        # Toast 提示
        self.config_toast = QtWidgets.QLabel("", self.config_tab)
        self.config_toast.setStyleSheet(f"""
            background-color: {COLORS['card_bg']};
            border: 1px solid {COLORS['border']};
            border-radius: 6px;
            padding: 6px 14px;
            color: {COLORS['text_primary']};
        """)
        self.config_toast.hide()
        self.config_toast_animation = QPropertyAnimation(self.config_toast, b"windowOpacity")
        self.config_toast_animation.finished.connect(self.config_toast.hide)

        # 绑定按钮事件
        self.btn_config_load.clicked.connect(self._load_config_to_ui)
        self.btn_config_save.clicked.connect(self._save_config_from_ui)
        self.btn_config_import.clicked.connect(self._import_config_bundle)
        self.btn_config_export.clicked.connect(self._export_all_config)
        self.btn_config_reset.clicked.connect(self._restore_default_config)
        self.btn_config_manage.clicked.connect(self._open_config_dialog)

    def _load_config_to_ui(self, silent=False):
        """从ConfigManager加载配置到界面"""
        try:
            config = self.config_manager.get_config()
            # model = config.get("dialog", {}).get("extra", {}).get("model", "O")
            index = self.asr_model.findText("男生")
            if index >= 0:
                self.asr_model.setCurrentIndex(index)

            self.speakers = config.get("model_speaker", {})
            if self.asr_model.currentText() == "男生":
                speaker = self.speakers.get("boys", [])
            else:
                speaker = self.speakers.get("girls", [])
            self.speaker.addItems(speaker)

            dialog_config = config.get("dialog", {})
            self.bot_name.setText(dialog_config.get("bot_name", ""))
            self.system_role.setPlainText(dialog_config.get("system_role", ""))
            self.kcb_content.setPlainText(config.get("kcb", ""))
            self.speaking_style.setPlainText(dialog_config.get("speaking_style", ""))
            self.sms_content.setPlainText(config.get("sms_content", ""))
            self.location_city.setText(dialog_config.get("location", {}).get("city", ""))
            self._append_log("配置已加载到界面")
            if not silent:
                self._show_config_toast("配置已加载到界面", True)
        except Exception as e:
            self._append_log(f"加载配置失败: {e}", "error")
            if not silent:
                self._show_config_toast(f"加载配置失败: {e}", False)

    def _save_config_from_ui(self):
        """从界面保存配置到ConfigManager"""
        try:
            speaker_name = self.speaker.currentText()
            if speaker_name == "薇薇":
                speaker_name = "zh_female_vv_jupiter_bigtts"
            elif speaker_name == "小天":
                speaker_name = "zh_male_xiaotian_jupiter_bigtts"
            elif speaker_name == "小荷":
                speaker_name = "zh_female_xiaohe_jupiter_bigtts"
            elif speaker_name=="云舟":
                speaker_name = "zh_male_yunzhou_jupiter_bigtts"
            else:
                speaker_name = "zh_male_yunzhou_jupiter_bigtts"
            config = {
                "asr": {
                    "extra": {
                        "end_smooth_window_ms": 1500
                    }
                },
                "kcb": self.kcb_content.toPlainText(),
                "sms_content": self.sms_content.toPlainText(),
                "model_speaker": {
                    "boys": ["云舟", "小天"],
                    "girls": ["薇薇", "小荷"]
                },
                "tts": {
                    "speaker": speaker_name,
                    "audio_config": {
                        "channel": 1,
                        "format": "pcm",
                        "sample_rate": 24000
                    }
                },
                "dialog": {
                    "bot_name": self.bot_name.text(),
                    "system_role": self.system_role.toPlainText(),
                    "speaking_style": self.speaking_style.toPlainText(),
                    "location": {
                        "city": self.location_city.text()
                    },
                    "extra": {
                        "strict_audit": False,
                        "audit_response": "",
                        "recv_timeout": 10,
                        "input_mod": "audio",
                        "model": self.asr_model.currentText()
                    }
                }
            }

            if self.config_manager.update_config(config):
                self._append_log("配置已保存到Redis和内存缓存")
                self._show_config_toast("配置已保存成功", True)
            else:
                raise Exception("保存配置失败")
        except Exception as e:
            self._append_log(f"保存配置失败: {e}", "error")
            self._show_config_toast(f"保存配置失败: {e}", False)


    def _reload_config_from_redis(self):
        """从Redis重新加载配置"""
        try:
            self.config_manager.reload_config()
            self._load_config_to_ui()
            self._append_log("已从Redis重新加载配置")
            self._show_config_toast("已从Redis重新加载配置", True)
        except Exception as e:
            self._append_log(f"重新加载配置失败: {e}")
            self._show_config_toast(f"重新加载配置失败: {e}", False)

    def _import_config_bundle(self):
        path = self._open_config_file_dialog("导入配置文件", QtWidgets.QFileDialog.AcceptOpen)
        if not path:
            return
        try:
            with open(path, "r", encoding="utf-8") as f:
                data = json.load(f)
            if isinstance(data, list):
                self._clear_device_cards()
                for item in data:
                    self._add_device_card(item.get("input_name", ""), item.get("output_name", ""), item.get("phone", ""))
                self._show_config_toast("配置导入成功", True)
            else:
                raise ValueError("文件格式不正确")
        except Exception as e:
            self._show_config_toast(f"导入失败: {e}", False)

    def _export_all_config(self):
        path = self._open_config_file_dialog("导出配置文件", QtWidgets.QFileDialog.AcceptSave)
        if not path:
            return
        try:
            data = self._collect_devices()
            with open(path, "w", encoding="utf-8") as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
            self._show_config_toast("配置已导出", True)
        except Exception as e:
            self._show_config_toast(f"导出失败: {e}", False)

    def _restore_default_config(self):
        try:
            self._clear_device_cards()
            self._load_config_to_ui()
            self._show_config_toast("已恢复默认配置", True)
        except Exception as e:
            self._show_config_toast(f"恢复失败: {e}", False)

    def _open_config_dialog(self):
        dialog = ConfigManagementDialog(self._collect_devices(), self)
        dialog.exec_()

    def _open_config_file_dialog(self, title: str, mode: QtWidgets.QFileDialog.AcceptMode) -> str:
        dialog = QtWidgets.QFileDialog(self, title)
        dialog.setAcceptMode(mode)
        dialog.setNameFilters(["配置文件 (*.json)", "所有文件 (*)"])
        dialog.setDefaultSuffix("json")
        dialog.setStyleSheet(f"""
            QWidget {{
                background-color: {COLORS['background']};
                color: {COLORS['text_primary']};
            }}
            QLabel {{
                color: {COLORS['text_secondary']};
            }}
            QPushButton {{
                background-color: #1f1f1f;
                color: {COLORS['text_primary']};
                border: 1px solid {COLORS['border']};
                border-radius: 6px;
                padding: 6px 12px;
            }}
            QPushButton:hover {{
                border-color: {COLORS['primary_orange']};
            }}
        """)
        if mode == QtWidgets.QFileDialog.AcceptSave:
            dialog.setWindowTitle("保存配置文件")
        else:
            dialog.setWindowTitle("加载配置文件")
        if dialog.exec_() == QtWidgets.QFileDialog.Accepted:
            return dialog.selectedFiles()[0]
        return ""

    def _show_config_toast(self, message: str, success: bool):
        color = COLORS['success'] if success else COLORS['error']
        self.config_toast.setText(message)
        self.config_toast.setStyleSheet(f"""
            background-color: {COLORS['card_bg']};
            border: 1px solid {COLORS['border']};
            border-radius: 6px;
            padding: 6px 14px;
            color: {color};
        """)
        self.config_toast.adjustSize()
        rect = self.config_tab.rect()
        x = rect.right() - self.config_toast.width() - 20
        y = rect.bottom() - self.config_toast.height() - 20
        self.config_toast.move(x, y)
        self.config_toast.setWindowOpacity(1.0)
        self.config_toast.show()
        self.config_toast.raise_()

        def start_fade():
            self.config_toast_animation.stop()
            self.config_toast_animation.setDuration(300)
            self.config_toast_animation.setStartValue(1.0)
            self.config_toast_animation.setEndValue(0.0)
            self.config_toast_animation.start()
        self.config_toast_animation.finished.connect(self.config_toast.hide)
        QtCore.QTimer.singleShot(3000, start_fade)


def main():
    # 🔴 关键修复：在创建QApplication后设置应用级高分屏属性
    app = QtWidgets.QApplication(sys.argv)

    # 设置高分屏适配（应用级属性，必须在创建窗口前设置）
    app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)  # 启用高分屏缩放
    app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)  # 使用高分屏图标

    # 启动加载动画
    loading_widget = LoadingWidget()
    loading_widget.move(
        app.desktop().screenGeometry().center().x() - loading_widget.width() // 2,
        app.desktop().screenGeometry().center().y() - loading_widget.height() // 2
    )
    loading_widget.show()
    app.processEvents()  # 刷新UI，显示加载动画

    # 创建主窗口（此时已设置好应用级属性）
    w = MainWindow()
    w.show()

    # 隐藏加载动画
    loading_widget.hide()

    sys.exit(app.exec_())




if __name__ == "__main__":
    task_thread = threading.Thread(target=main_task, daemon=True)
    task_thread.start()

    # Run main() in the main thread (keeps GUI responsive)
    main()